package bsh;

import java.util.List;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import bsh.asm.acessor.ObjectAccessor;
import bsh.exception.CompilationAssertionFailed;
import bsh.exception.CompilationFailedException;
import bsh.meta.ArrayTypeWrapper;
import bsh.meta.JavaClassWrapper;
import bsh.meta.JavaMethodDescriptor;
import bsh.meta.MethodDescriptor;
import bsh.meta.Scope;
import bsh.meta.TypeWrapper;
import bsh.meta.UnfinishedInnerType;
import bsh.utils.reflection.BytecodeHelper;

/**
 * compila 'new'
 * 
 * @author takeshi
 */
public class AllocationNodeCompiler implements NodeCompiler {

    private static final int NUM_3 = 3;

    private final BSHAllocationExpression node;

    private final CompilationUnit unit;

    private TypeWrapper type;

    /**
     * @param node node
     * @param unit unit
     * @throws CompilationFailedException e
     */
    public AllocationNodeCompiler(BSHAllocationExpression node, CompilationUnit unit) throws CompilationFailedException {
        super();
        this.node = node;
        this.unit = unit;
        this.type = findNodeType(node, unit, unit.getGlobalScope());
    }

    /**
     * Procura o tipo
     * 
     * @param node node
     * @param unit unit
     * @param scope the declaring {@link Scope}
     * @return {@link TypeWrapper}
     * @throws CompilationFailedException e
     */
    private static TypeWrapper findNodeType(BSHAllocationExpression node, CompilationUnit unit, Scope scope)
            throws CompilationFailedException {
        SimpleNode typeNode = node.getChild(0);
        SimpleNode args = node.getChild(1);
        if (typeNode instanceof BSHAmbiguousName) {
            TypeWrapper baseType = scope.resolveType(((BSHAmbiguousName) typeNode).text);
            if (baseType == null) {
                return null;
                // throw new CompilationFailedException(node.getLineNumber(), "Cannot find type: " + ((BSHAmbiguousName) typeNode).text);
            }
            if (args instanceof BSHArguments) {
                return baseType;
            } else {
                return arrayType(baseType, (BSHArrayDimensions) args);
            }
        } else {
            BSHArrayDimensions dim = (BSHArrayDimensions) args;
            TypeWrapper arrayType = FormalParameterParser.extractType(typeNode, scope);
            return arrayType(arrayType, dim);
        }
    }

    /**
     * @param arrayType {@link TypeWrapper}
     * @param dim dimensoes
     * @return {@link TypeWrapper}
     */
    private static TypeWrapper arrayType(TypeWrapper arrayType, BSHArrayDimensions dim) {
        TypeWrapper theType = arrayType;
        for (int i = dim.numDefinedDims + dim.numUndefinedDims; i > 0; i--) {
            theType = new ArrayTypeWrapper(theType);
        }
        return theType;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.NodeCompiler#compile(org.objectweb.asm.MethodVisitor, bsh.CompilationState)
     */
    public void compile(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        if (this.type == null) {
            this.type = findNodeType(node, unit, state.currentScope());
            if (this.type == null) {
                SimpleNode typeNode = node.getChild(0);
                throw new CompilationFailedException(node.getLineNumber(), "Cannot find type: " + ((BSHAmbiguousName) typeNode).text);
            }
        }
        if (this.type instanceof ArrayTypeWrapper) {
            ArrayTypeWrapper arrayType = (ArrayTypeWrapper) this.type;
            this.compileArrayType(host, state, arrayType, (BSHArrayDimensions) this.node.getChild(1));
        } else if (this.node.jjtGetNumChildren() == 2) {
            this.compileConstructorCall(host, state, this.type, (BSHArguments) this.node.getChild(1));
        } else {
            TypeWrapper superType = JavaClassWrapper.OBJECT_WRAPPER;
            TypeWrapper implemented = this.type;
            if (!this.type.isInterface()) {
                implemented = null;
                superType = this.type;
            }
            this.compileAnnonymousInnerClass(host, state, superType, implemented, (BSHArguments) this.node.getChild(1),
                    (BSHBlock) this.node.getChild(2));
        }
    }

    /**
     * Compiles an annonymous inner class
     * 
     * @param host the declaring {@link MethodVisitor}
     * @param state the declaration site {@link CompilationState}
     * @param superType the super type of the annonymous inner class
     * @param implemented the interface implemented by the inner class if any. May be null.
     * @param ctorArguments the constructor arguments passed to the inner class.
     * @param classBlock the class block with the body of the class
     * @throws CompilationFailedException e
     */
    private void compileAnnonymousInnerClass(MethodVisitor host, CompilationState state, TypeWrapper superType, TypeWrapper implemented,
            BSHArguments ctorArguments, BSHBlock classBlock) throws CompilationFailedException {
        String clName = this.unit.thisType().internalName() + "$" + this.unit.thisType().newDynamicInvocationId();
        UnfinishedInnerType innerClass =
                this.unit.getCompilerManager().compileInnerClass(clName, null, this.unit, classBlock, state.currentScope(), superType,
                        implemented == null ? new TypeWrapper[]{} : new TypeWrapper[]{ implemented }, Opcodes.ACC_PUBLIC);
        compileConstructorCallForInnerClass(host, state, ctorArguments, innerClass);
    }

    /**
     * Finds the constructor used by the inner class and calls it, prepending the external variables. If a constructor cannot be found with
     * the given static information, this it delegates to a runtime call to {@link CompiledScript#createObject(int, Class, Object[])}
     * 
     * @param host declaring {@link MethodVisitor}
     * @param state declaring {@link CompilationState}
     * @param ctorArguments the constructor arguments passed to the inner class
     * @param innerClass the {@link UnfinishedInnerType}
     * @throws CompilationFailedException e
     */
    public void compileConstructorCallForInnerClass(MethodVisitor host, CompilationState state, BSHArguments ctorArguments,
            UnfinishedInnerType innerClass) throws CompilationFailedException {
        TypeWrapper[] arguments = MethodCallCompiler.resolveParamTypes(ctorArguments, unit, state.currentScope());
        List<String> syntheticVariables = innerClass.externalVariables();
        TypeWrapper[] actualArguments = new TypeWrapper[1 + syntheticVariables.size() + arguments.length];
        actualArguments[0] = this.unit.thisType();
        for (int i = 0; i < syntheticVariables.size(); i++) {
            actualArguments[i + 1] = state.currentScope().getAccessor(syntheticVariables.get(i)).type();
        }
        System.arraycopy(arguments, 0, actualArguments, 1 + syntheticVariables.size(), arguments.length);
        List<? extends TypeWrapper> ctorParams = this.unit.reflectionUtils().findConstructorFor(innerClass, actualArguments);
        if (ctorParams != null) {
            host.visitTypeInsn(Opcodes.NEW, innerClass.internalName());
            host.visitInsn(Opcodes.DUP);
            unit.thisType().loadThis(host, state);
            for (int i = 0; i < syntheticVariables.size(); i++) {
                ObjectAccessor acc = state.currentScope().getAccessor(syntheticVariables.get(i));
                acc.loadObject(host, state);
            }
            state.addLineNumber(host, ctorArguments.getLineNumber());
            for (int i = 0; i < ctorArguments.jjtGetNumChildren(); i++) {
                final SimpleNode child = ctorArguments.getChild(i);
                final NodeCompiler compiler = unit.getNodeCompiler(child);
                if (compiler == null) {
                    throw new CompilationFailedException(ctorArguments.getLineNumber(), "cannot compile node: " + child + " of type: "
                            + child.getClass());
                }
                final TypeWrapper resultType = unit.getNodeType(child, state.currentScope());
                compiler.compile(host, state);
                unit.getCompilerManager().coerceToType(host, resultType, ctorParams.get(1 + i + syntheticVariables.size()), state);
            }
            compileConstructorInvocation(host, state, innerClass, ctorParams);
        } else {
            int oldStackSize = state.getStackSize();
            this.unit.thisType().loadThis(host, state);

            BytecodeHelper.pushIntConst(state.getUnit().thisType().newDynamicInvocationId(), host);
            state.pushStack(int.class);

            host.visitLdcInsn(innerClass.toASMType());
            state.pushStack(JavaClassWrapper.wrapperFor(Class.class));

            int totalArguments = 1 + syntheticVariables.size() + ctorArguments.jjtGetNumChildren();

            BytecodeHelper.pushIntConst(totalArguments, host);
            TypeWrapper targetType = JavaClassWrapper.OBJECT_WRAPPER;
            host.visitTypeInsn(Opcodes.ANEWARRAY, targetType.internalName());
            state.pushStack(new ArrayTypeWrapper(JavaClassWrapper.OBJECT_WRAPPER));

            prepareArrayElementAccess(host, state, 0);
            unit.thisType().loadThis(host, state);
            doStoreArray(host, state);

            for (int i = 0; i < syntheticVariables.size(); i++) {
                prepareArrayElementAccess(host, state, i + 1);
                ObjectAccessor acc = state.currentScope().getAccessor(syntheticVariables.get(i));
                acc.loadObject(host, state);
                doStoreArray(host, state);
            }
            for (int i = 0; i < ctorArguments.jjtGetNumChildren(); i++) {
                prepareArrayElementAccess(host, state, i + 1 + syntheticVariables.size());
                state.getUnit().compileNode(ctorArguments.getChild(i), host, state);
                state.getUnit().getCompilerManager().coerceToType(host, state.peekStack(), targetType, state);
                doStoreArray(host, state);
            }
            state.assertStackSize(oldStackSize + NUM_3 + 1);
            // must write the code to invoke new
            DYNAMIC_CREATION_METHOD_DESCRIPTOR.writeInvoke(host, state);
            state.assertStackSize(oldStackSize + 1);
            // we prolly must typecheck
            this.unit.getCompilerManager().coerceStackTopTo(host, innerClass, state);
            state.assertStackSize(oldStackSize + 1);

        }
    }

    /**
     * Prepares to read or write to an array that is on the stack. Dups the array and pushes the array index so the array remains on the
     * stack
     * 
     * @param host the {@link MethodVisitor}
     * @param state the {@link CompilationState}
     * @param arrayIndex the array index to store to / read from
     */
    private static void prepareArrayElementAccess(MethodVisitor host, CompilationState state, int arrayIndex) {
        // stack top must be an array
        if (!(state.peekStack() instanceof ArrayTypeWrapper)) {
            throw new CompilationAssertionFailed("must be an array");
        }
        host.visitInsn(Opcodes.DUP);
        state.pushStack(state.peekStack());
        BytecodeHelper.pushIntConst(arrayIndex, host);
        state.pushStack(int.class);
    }

    /**
     * Actually stores a value to the array. Pops 3 values from the stack.
     * 
     * @param host the {@link MethodVisitor}
     * @param state the {@link CompilationState}
     */
    private static void doStoreArray(MethodVisitor host, CompilationState state) {
        state.assertMinimumStackSize(NUM_3);
        TypeWrapper objectInstance = state.popStack();
        TypeWrapper index = state.popStack();
        if (!index.equals(JavaClassWrapper.INT_WRAPPER)) {
            throw new CompilationAssertionFailed("Required an int on the stack, but was: " + index);
        }
        TypeWrapper array = state.popStack();
        TypeWrapper arrayComponent = ((ArrayTypeWrapper) array).getComponentType();
        if (!arrayComponent.isSuperTypeOf(objectInstance)) {
            throw new CompilationAssertionFailed("type " + arrayComponent + " cannot store: " + objectInstance);
        }
        int opcode = arrayComponent.toASMType().getOpcode(Opcodes.IASTORE);
        host.visitInsn(opcode);
    }

    /**
     * Compiles a constructor invocation
     * 
     * @param host the {@link MethodVisitor}
     * @param state the {@link CompilationState}
     * @param declaring the {@link TypeWrapper} to be constructed
     * @param ctorParams the constructor parameters
     */
    public static void compileConstructorInvocation(MethodVisitor host, CompilationState state, TypeWrapper declaring,
            List<? extends TypeWrapper> ctorParams) {
        Type[] asmTypes = toASMTypes(ctorParams);
        host.visitMethodInsn(Opcodes.INVOKESPECIAL, declaring.internalName(), "<init>",
                Type.getMethodDescriptor(Type.getType(void.class), asmTypes));
        state.popStack(asmTypes.length);
        state.pushStack(declaring);
    }

    /**
     * Compila um construtor
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @param declaring {@link TypeWrapper} a ser construido
     * @param arguments arguments
     * @throws CompilationFailedException e
     */
    private void compileConstructorCall(MethodVisitor host, CompilationState state, TypeWrapper declaring, BSHArguments arguments)
            throws CompilationFailedException {
        state.addLineNumber(host, this.node.getLineNumber());
        TypeWrapper[] args = MethodCallCompiler.resolveParamTypes(arguments, this.unit, state.currentScope());
        if (declaring instanceof UnfinishedInnerType && !declaring.isStatic()) {
            this.compileConstructorCallForInnerClass(host, state, arguments, (UnfinishedInnerType) declaring);
        } else {
            List<? extends TypeWrapper> ctorParams = this.unit.reflectionUtils().findConstructorFor(declaring, args);
            if (ctorParams == null) {
                this.compileDynamicAllocation(host, state, declaring, arguments);
                return;
            }
            host.visitTypeInsn(Opcodes.NEW, declaring.internalName());
            host.visitInsn(Opcodes.DUP);
            // currently, varargs on constructors are not statically supported
            MethodCallCompiler.pushArgumentsFor(host, arguments, ctorParams, state, this.unit, false);
            compileConstructorInvocation(host, state, declaring, ctorParams);

        }

    }

    /**
     * Compila uma alocacao dinamica - vai usar reflection em runtime para construir o objeto
     * 
     * @param host o {@link MethodVisitor}
     * @param state o {@link CompilationState}
     * @param declaring o tipo a ser construido
     * @param arguments os argumentos
     * @throws CompilationFailedException e
     */
    private void compileDynamicAllocation(MethodVisitor host, CompilationState state, TypeWrapper declaring, BSHArguments arguments)
            throws CompilationFailedException {
        int oldStackSize = state.getStackSize();
        this.unit.thisType().loadThis(host, state);

        BytecodeHelper.pushIntConst(state.getUnit().thisType().newDynamicInvocationId(), host);
        state.pushStack(int.class);

        host.visitLdcInsn(declaring.toASMType());
        state.pushStack(JavaClassWrapper.wrapperFor(Class.class));

        pushArgumentsToArray(host, state, arguments);
        state.assertStackSize(oldStackSize + NUM_3 + 1);
        // must write the code to invoke new
        DYNAMIC_CREATION_METHOD_DESCRIPTOR.writeInvoke(host, state);
        state.assertStackSize(oldStackSize + 1);
        // we prolly must typecheck
        this.unit.getCompilerManager().coerceStackTopTo(host, declaring, state);
        state.assertStackSize(oldStackSize + 1);
    }

    /**
     * Pushes arguments to an array, boxing, unboxing and casting if necessary. It is used to call
     * {@link CompiledScript#createObject(int, Class, Object[])}
     * 
     * @param host the {@link MethodVisitor}
     * @param state the {@link CompilationState}
     * @param arguments the {@link BSHArguments}
     * @throws CompilationFailedException e
     */
    public static void pushArgumentsToArray(MethodVisitor host, CompilationState state, BSHArguments arguments)
            throws CompilationFailedException {
        final int total = arguments.jjtGetNumChildren();
        BytecodeHelper.pushIntConst(total, host);
        TypeWrapper targetType = JavaClassWrapper.OBJECT_WRAPPER;
        host.visitTypeInsn(Opcodes.ANEWARRAY, targetType.internalName());
        state.pushStack(new ArrayTypeWrapper(JavaClassWrapper.OBJECT_WRAPPER));
        for (int i = 0; i < arguments.jjtGetNumChildren(); i++) {
            prepareArrayElementAccess(host, state, i);
            state.getUnit().compileNode(arguments.getChild(i), host, state);
            state.getUnit().getCompilerManager().coerceToType(host, state.peekStack(), targetType, state);
            doStoreArray(host, state);
        }
    }

    private static final MethodDescriptor DYNAMIC_CREATION_METHOD_DESCRIPTOR;
    static {
        try {
            DYNAMIC_CREATION_METHOD_DESCRIPTOR =
                    new JavaMethodDescriptor(CompiledScript.class.getDeclaredMethod("createObject", int.class, Class.class, Object[].class));
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    /**
     * Converte os parametros para Type[]
     * 
     * @param ctorParams parametros
     * @return Type[]
     */
    private static Type[] toASMTypes(List<? extends TypeWrapper> ctorParams) {
        Type[] types = new Type[ctorParams.size()];
        for (int i = 0; i < types.length; i++) {
            types[i] = ctorParams.get(i).toASMType();
        }
        return types;
    }

    /**
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @param arrayType {@link ArrayTypeWrapper}
     * @param arrayDimensions {@link BSHArrayDimensions}
     * @throws CompilationFailedException e
     */
    private void compileArrayType(MethodVisitor host, CompilationState state, ArrayTypeWrapper arrayType, BSHArrayDimensions arrayDimensions)
            throws CompilationFailedException {
        SimpleNode maybeInitializer = arrayDimensions.getChild(0);
        if (maybeInitializer instanceof BSHArrayInitializer) {
            ArrayInitializerNodeCompiler compiler = (ArrayInitializerNodeCompiler) this.unit.getNodeCompiler(maybeInitializer);
            compiler.compileArrayInitialization(host, state, arrayType);
        } else {
            int oldStack = state.getStackSize();
            this.unit.compileNode(maybeInitializer, host, state);
            state.assertStackSize(oldStack + 1);
            this.unit.getCompilerManager().coerceStackTopTo(host, JavaClassWrapper.INT_WRAPPER, state);
            state.assertStackSize(oldStack + 1);
            final TypeWrapper componentType = arrayType.getComponentType();
            if (componentType instanceof JavaClassWrapper) {
                ((JavaClassWrapper) componentType).allocatePrimitiveArray(host);
            } else {
                host.visitTypeInsn(Opcodes.ANEWARRAY, componentType.internalName());
            }
            state.popStack();
            state.pushStack(arrayType);
            state.assertStackSize(oldStack + 1);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.NodeCompiler#nodeResultType(bsh.meta.Scope)
     */
    public TypeWrapper nodeResultType(Scope scope) throws CompilationFailedException {
        if (type == null) {
            this.type = findNodeType(node, unit, scope);
        }
        return this.type;
    }

}
